﻿#region Disclaimer/Info
///////////////////////////////////////////////////////////////////////////////////////////////////
// MetaCollection
// 
// MetaCollection is a system to provide a standard structure, permissions and metadata service
// for content organization.
//
// This project is hosted at Google Code at http://code.google.com/p/metacollection/
//
// This project is licensed under the BSD license.  See the License.txt file for more information.
///////////////////////////////////////////////////////////////////////////////////////////////////
#endregion

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Web;
using System.Xml;
using System.Xml.Linq;
using MetaCollection.Framework;
using MetaCollection.Framework.Model;

namespace MetaCollection.ContentService.WebDav.Methods
{
	/// <summary>
	/// The PROPPATCH method processes instructions specified in the request body to set 
	/// and/or remove properties defined on the resource identified by the Request-URI.
	/// </summary>
	/// <example>
	/// 
	/// >>Request
	///
	///   PROPPATCH /bar.html HTTP/1.1
	///   Host: www.foo.com
	///   Content-Type: text/xml; charset="utf-8"
	///   Content-Length: xxxx
	///
	///   <?xml version="1.0" encoding="utf-8" ?>
	///   <D:propertyupdate xmlns:D="DAV:"
	///   xmlns:Z="http://www.w3.com/standards/z39.50/">
	///     <D:set>
	///          <D:prop>
	///               <Z:authors>
	///                    <Z:Author>Jim Whitehead</Z:Author>
	///                    <Z:Author>Roy Fielding</Z:Author>
	///               </Z:authors>
	///          </D:prop>
	///     </D:set>
	///     <D:remove>
	///          <D:prop><Z:Copyright-Owner/></D:prop>
	///     </D:remove>
	///   </D:propertyupdate>
	///
	/// >>Response
	///
	///   HTTP/1.1 207 Multi-Status
	///   Content-Type: text/xml; charset="utf-8"
	///   Content-Length: xxxx
	///
	///   <?xml version="1.0" encoding="utf-8" ?>
	///   <D:multistatus xmlns:D="DAV:"
	///   xmlns:Z="http://www.w3.com/standards/z39.50">
	///     <D:response>
	///          <D:href>http://www.foo.com/bar.html</D:href>
	///          <D:propstat>
	///               <D:prop><Z:Authors/></D:prop>
	///               <D:status>HTTP/1.1 424 Failed Dependency</D:status>
	///          </D:propstat>
	///          <D:propstat>
	///               <D:prop><Z:Copyright-Owner/></D:prop>
	///               <D:status>HTTP/1.1 409 Conflict</D:status>
	///          </D:propstat>
	///          <D:responsedescription> Copyright Owner can not be deleted or
	///   altered.</D:responsedescription>
	///     </D:response>
	///   </D:multistatus>
	///</example>
	/// <remarks>
	/// Servers MUST process PROPPATCH instructions in document order (an exception to
	/// the normal rule that ordering is irrelevant). Instructions MUST either all be
	/// executed or none executed. Thus, if any error occurs during processing, all
	/// executed instructions MUST be undone and a proper error result returned.
	/// </remarks>
	public static class PropPatch
	{

		public static void Respond(DavContext context, IRequestHandler handler)
		{
			IEnumerable<XElement> setProperties;
			IEnumerable<XElement> removedProperties;

			context.Response.SetCacheHeaders(HttpCacheability.ServerAndPrivate, DateTime.Now);
			RequestedProperties(context.Request.Xml, out setProperties, out removedProperties);
			context.Response.OutputXml(GetItemsToUpdate(context, handler, context.Item, context.Request.Depth()).ApplyChanges(context, handler, setProperties, removedProperties));
		}

		/// <summary>
		/// Gets the items to update.
		/// </summary>
		/// <param name="handler">The handler to use for requesting the items.</param>
		/// <param name="item">The source item (for finding children).</param>
		/// <param name="includeChildren">if <c>true</c> then include the direct children of the item, 
		/// if <c>false</c> then don't include any children... if <c>null</c> then include all children</param>
		/// <returns></returns>
		private static IEnumerable<IItemInstance> GetItemsToUpdate(DavContext context, IRequestHandler handler, IItemInstance item, DepthOption depth)
		{
			if (depth == DepthOption.AllDescendants)
				return (new List<IItemInstance>() { item }).Concat(handler.ItemRequest(context.User, RequestTypes.GetItem | RequestTypes.ProcessAllDescendents, item.Href));
			else if (depth == DepthOption.ImmediateDescendants || depth == DepthOption.Undefined)
				return (new List<IItemInstance>() { item }).Concat(handler.ItemRequest(context.User, RequestTypes.GetItem | RequestTypes.ProcessDirectDescendents, item.Href));
			else
				return new List<IItemInstance>() { item };
		}

		/// <summary>
		/// Gets the requested property changes.
		/// </summary>
		/// <param name="requestXmlNavigator">The request XML navigator.</param>
		/// <returns>A collection of properties requested for change or delete in the PROPFIND request</returns>
		private static void RequestedProperties(XElement requestXml, out IEnumerable<XElement> setProperties, out IEnumerable<XElement> removedProperties)
		{
			setProperties = new List<XElement>();
			removedProperties = new List<XElement>();
			if (requestXml != null)
			{
				var setProps = requestXml.Elements(XName.Get("set", "DAV:")).FirstOrDefault();
				var delProps = requestXml.Elements(XName.Get("remove", "DAV:")).FirstOrDefault();

				if (setProps != null)
					setProperties = from n in setProps.Elements(XName.Get("prop", "DAV:")).Elements() where n.NodeType == XmlNodeType.Element select n;

				if (delProps != null)
					removedProperties = from n in delProps.Elements(XName.Get("prop", "DAV:")).Elements() where n.NodeType == XmlNodeType.Element select n;
			}
		}

		/// <summary>
		/// Applies the changes to the set of items.
		/// </summary>
		/// <param name="items">The items.</param>
		/// <param name="handler">The item request handler to use for saving items.</param>
		/// <param name="baseUri">The base URI.</param>
		/// <param name="setProperties">The set properties.</param>
		/// <param name="removedProperties">The removed properties.</param>
		/// <returns>
		/// A set of Response/PropStat webdav elements, one propstat for each change and one response for each item
		/// </returns>
		private static IEnumerable<XElement> ApplyChanges(this IEnumerable<IItemInstance> items, DavContext context, IRequestHandler handler, IEnumerable<XElement> setProperties, IEnumerable<XElement> removedProperties)
		{
			PropertyNameComparer comparer = new PropertyNameComparer();
			// apply changes/save (out change status set) --> build response (set) --> outputresponse
			foreach (var item in items)
			{
				IDictionary<XName, IResourceProperty> modelProperties = Properties.ItemSpecificationExplicit(context, item.Specification, item.Href).ToDictionary(p => p.Name);
				IEnumerable<XElement> propStats = null;
				IEnumerable<XName> invalidProperties = from p in setProperties.Concat(removedProperties)
													   where !modelProperties.Keys.Contains(p.Name, comparer)
													   select p.Name;
				EvaluateVersionLocking(item);

				XElement failingProperty = null;
				bool invalidRequest = invalidProperties.Any();
				int? version = null;
				string responseMessage = invalidRequest ? "Adding properties that are not part of the Item Specification is Forbidden" : String.Empty;

				if (!invalidRequest)
					failingProperty = ProcessSetProperties(handler, setProperties, item, modelProperties, ref invalidRequest);
				if (!invalidRequest)
					failingProperty = ProcessRemovedProperties(removedProperties, item, modelProperties, ref invalidRequest);
				if (!invalidRequest)
					responseMessage = SaveItem(context, handler, item, ref invalidRequest, out version);

				// build a list of all of the properties in the request except those identified as invalid or the one failing property. set their propstatus to success or fail as appropriate
				propStats = from p in setProperties.Concat(removedProperties)
							where !comparer.Equals(p.Name, failingProperty.PropStatName()) && modelProperties.Keys.Contains(p.Name, comparer)
							select p.Name.AsPropStat(invalidRequest ? "HTTP/1.1 424 Failed Dependency" : "HTTP/1.1 200 OK");

				string href = item.Href.ToWebsiteUri().AbsoluteUri;
				if (version.HasValue)
					href += "?ver=" + version.Value.ToString();
				XElement response = new XElement(XName.Get("response", "DAV:"),
					new XAttribute(XNamespace.Xmlns + "mcs", ItemNamespace(item.Specification)),
					new XElement(XName.Get("href", "DAV:"), href),
					propStats, invalidProperties.AsPropStat("HTTP/1.1 403 Forbidden")); // we use forbidden because we do not allow arbitrary properties to be added. TODO : use the Model allow unknown for override.

				if (invalidRequest && failingProperty != null)
					response.Add(failingProperty);
				if (!string.IsNullOrEmpty(responseMessage))
					response.Add(new XElement(XName.Get("responsedescription", "DAV:"), responseMessage));

				yield return response;
			}
		}

		/// <summary>
		/// Evaluates the version locking.  If the current version is published then the current item is
		/// patched so that a new version will be created.  Also checks the lock on the item and if the 
		/// current user does not own the lock or the lock has expired then a new version is created.
		/// </summary>
		/// <param name="item">The item to check the published/locking status of and update as needed for saving.</param>
		private static void EvaluateVersionLocking(IItemInstance item)
		{
			bool userOwnsLock = LockProperty.CurrentUserOwnsLock(item);
			if (item.Published || !userOwnsLock)
			{
				item.LeftVersion = item.Version;
				item.Version = 0;
				item.Published = false;
				item.Properties = item.Properties.PatchProperty(Properties.CONTENTSERVICE_LOCK_PROPERTY_NAME, LockProperty.GetNewLockForCurrentUser());
			}
			// if the user owns the lock then update it. TODO: this should ideally just update the expires on the lock not fully replace it.  Also, if the lock has depth then many items will need to be patched.]
			if (userOwnsLock)
				item.Properties = item.Properties.PatchProperty(Properties.CONTENTSERVICE_LOCK_PROPERTY_NAME, LockProperty.GetNewLockForCurrentUser());
		}

		/// <summary>
		/// Saves the item.
		/// </summary>
		/// <param name="handler">The handler to process the save item request.</param>
		/// <param name="item">The item.</param>
		/// <param name="invalidRequest">if set to <c>true</c> [invalid request].</param>
		/// <returns></returns>
		private static string SaveItem(DavContext context, IRequestHandler handler, IItemInstance item, ref bool invalidRequest, out int? version)
		{
			version = null;
			string status = String.Empty;
			try
			{
				item.LastModified = DateTime.Now;
				IItemInstance savedItem = handler.ItemRequest(context.User, RequestTypes.SetItem, item.Href, item).FirstOrDefault();
				if (savedItem != null)
				{
					version = savedItem.Version;
					status = String.Format(CultureInfo.CurrentCulture, "Successfully saved '{0}' with version {1}", savedItem.Name, savedItem.Version);
				}
			}
			catch (InvalidOperationException iEx)
			{
				status = iEx.Message;
				invalidRequest = true;
			}
			return status;
		}

		/// <summary>
		/// Processes the removed properties.
		/// </summary>
		/// <param name="removedProperties">The removed properties.</param>
		/// <param name="item">The item to remove the properties from.</param>
		/// <param name="modelProperties">The model properties of the item.</param>
		/// <param name="invalidRequest">if set to <c>true</c> [invalid request].</param>
		/// <returns></returns>
		private static XElement ProcessRemovedProperties(IEnumerable<XElement> removedProperties, IItemInstance item, IDictionary<XName, IResourceProperty> modelProperties, ref bool invalidRequest)
		{
			string status = String.Empty;
			foreach (var delProp in removedProperties)
			{
				// relationships have to have content, everything else should be empty.
				if ((!modelProperties[delProp.Name].ItemPropertyName.Equals("RELATIONSHIPS", StringComparison.OrdinalIgnoreCase)) &&
					((delProp.HasElements) || (!string.IsNullOrEmpty(delProp.Value))))
				{
					invalidRequest = true;
					status = "HTTP/1.1 400 Bad Request - Removed Properties Can Not Have Content";
				}
				else
					invalidRequest |= !SetItemProperty(item, modelProperties[delProp.Name].ItemPropertyName, delProp, false, out status);

				if (invalidRequest)
					return delProp.Name.AsPropStat(status);
			}
			return null;
		}
		/// <summary>
		/// Processes the set properties.
		/// </summary>
		/// <param name="setProperties">The set properties.</param>
		/// <param name="item">The item to set the properties of.</param>
		/// <param name="modelProperties">The model properties of the item.</param>
		/// <param name="invalidRequest">if set to <c>true</c> [invalid request].</param>
		/// <returns>a status description associated with the processing.  Generally this can be ignored unless invalidRequest is <c>true</c></returns>
		private static XElement ProcessSetProperties(IRequestHandler handler, IEnumerable<XElement> setProperties, IItemInstance item, IDictionary<XName, IResourceProperty> modelProperties, ref bool invalidRequest)
		{
			string status = String.Empty;
			foreach (var setProp in setProperties)
			{
				string propertyName = modelProperties[setProp.Name].ItemPropertyName;
				//if (!item.Name.Equals(setProp.Value, StringComparison.OrdinalIgnoreCase))
				//{
				//    Uri parentUri = new Uri(String.Format("{0}/{1}", item.Href.AbsoluteUri.Substring(0, item.Href.AbsoluteUri.LastIndexOf('/')), setProp.Value));
				//    IItemInstance itemWithSameName = handler.ItemRequest(RequestTypes.GetItem, parentUri, MetaCollection.Framework.Model.LoadOptions.ItemReference).FirstOrDefault();
				//    if (itemWithSameName != null)
				//        invalidRequest = true;
				//}

				invalidRequest |= !SetItemProperty(item, propertyName, setProp, true, out status);
				if (invalidRequest)
					return setProp.Name.AsPropStat(status);
			}
			return null;
		}

		/// <summary>
		/// Returns a PropStat web dav element with the given status for each name in Names
		/// </summary>
		/// <param name="names">The names of each property.</param>
		/// <param name="status">The status to use for each element.</param>
		/// <returns>a set of propstat elements for each name</returns>
		private static IEnumerable<XElement> AsPropStat(this IEnumerable<XName> names, string status)
		{
			foreach (var name in names)
			{
				yield return name.AsPropStat(status);
			}
		}

		/// <summary>
		/// Returns a propstat element for the given element name with the status
		/// </summary>
		/// <param name="name">The name of the property element this propstat is for.</param>
		/// <param name="status">The status of the element</param>
		/// <returns>a propstat web dav element</returns>
		private static XElement AsPropStat(this XName name, string status)
		{
			return new XElement(XName.Get("propstat", "DAV:"),
						new XElement(XName.Get("prop", "DAV:"),
							new XElement(name)),
						new XElement(XName.Get("status", "DAV:"), status)
					);
		}

		/// <summary>
		/// Return the name of the property contained within the propstat webdav element.
		/// </summary>
		/// <param name="propStatElement">The prop stat element.</param>
		/// <returns>The name of the property in the propstat element.</returns>
		private static XName PropStatName(this XElement propStatElement)
		{
			if (propStatElement == null)
				return null;
			else
				return (from n in propStatElement.Elements(XName.Get("prop", "DAV:"))
						where n.HasElements
						select n.Elements().FirstOrDefault().Name).FirstOrDefault();
		}

		/// <summary>
		/// Assigns the MC property.
		/// </summary>
		/// <param name="item">The item.</param>
		/// <param name="propertyName">Name of the property.</param>
		/// <param name="propertyValue">The property value.</param>
		/// <returns></returns>
		private static bool SetItemProperty(IItemInstance item, string itemPropertyName, XElement property, bool IsSet, out string status)
		{
			status = "HTTP/1.1 200 OK";
			if (itemPropertyName.StartsWith("PROPERTIES|", StringComparison.OrdinalIgnoreCase))
			{
				item.Properties = item.Properties.PatchProperty(itemPropertyName, property.Value);
				return true;
			}

			switch (itemPropertyName.ToUpperInvariant())
			{
				case "ID": status = "modifying this property is not supported."; return false;
				case "EXTERNALID": item.ExternalId = property.Value; return true;
				case "PARENTID": item.ParentId = new Guid(property.Value); ; return true;
				case "VERSION": status = "cannot-modify-protected-property"; return false;
				case "LEFTVERSION": item.LeftVersion = Convert.ToInt32(property.Value, CultureInfo.InvariantCulture); return true;
				case "RIGHTVERSION": item.RightVersion = Convert.ToInt32(property.Value, CultureInfo.InvariantCulture); return true;
				case "CREATEDBY": status = "cannot-modify-protected-property"; return false;
				case "NAME": item.Name = property.Value; return true;
				case "DISPLAYNAME": item.Name = property.Value; return true;
				case "PUBLISHED": item.Published = Convert.ToBoolean(property.Value, CultureInfo.InvariantCulture); return true;
				case "MIMETYPE": item.MimeType = property.Value; return true;
				case "SPECIFICATION": item.Specification = new Guid(property.Value); return true;
				case "RELATIONSHIPS": item.Relationships = Properties.UpdateRelationships(item.Relationships, property, IsSet); return true;
				case "PERMISSIONS": throw new NotImplementedException();
				default: status = "modifying this property is not supported."; return false;
			}
		}

		/// <summary>
		/// Converts the Item Specification Id into an items namespace.
		/// </summary>
		/// <param name="providerId">The item Specification id.</param>
		/// <returns>A namespace uri for the given item specification</returns>
		private static string ItemNamespace(Guid itemSpecificationId)
		{
			return String.Format(CultureInfo.CurrentCulture, "uri://MetaCollection/v2/spec/{0}", itemSpecificationId);
		}
	}
}
